/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.cdflynn.ohos.library.turn;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ListContainer;
import ohos.agp.utils.Point;
import ohos.app.Context;

/**
 * TurnListContainer
 *
 * @author:hello
 * @since 2021-04-25
 */
public class TurnListContainer extends ListContainer implements ListContainer.ScrolledListener,
        Component.LayoutRefreshedListener {
    private static final int MIN_RADIUS = 0;
    private static final int MIN_PEEK = 0;
    private static final int HALF = 2;
    private static final float PART = 2.0f;
    private static final int COEFFICIENT = -1;
    private static final int DEFAULT_RADIUS = 1200;
    private static final int DEFAULT_DISTANCE = 300;
    @Gravity
    private int gravity;
    private int radius = DEFAULT_RADIUS;
    private int peekDistance = DEFAULT_DISTANCE;
    private boolean isRotate;
    private Point center;

    /**
     * onTouchEvent
     *
     * @param context
     * @param attrSet
     */
    public TurnListContainer(Context context, AttrSet attrSet) {
        super(context, attrSet);
        center = new Point();
        setScrolledListener(this);
        setLayoutRefreshedListener(this);
    }

    /**
     * Gravity
     *
     * @author:hello
     * @since 2021-04-25
     */
    public @interface Gravity {
        /**
         * START
         */
        int START = Component.DRAG_LEFT;
        /**
         * END
         */
        int END = Component.DRAG_RIGHT;
    }

    /**
     * Orientation
     *
     * @author:hello
     * @since 2021-04-25
     */
    public @interface Orientation {
        /**
         * VERTICAL
         */
        int VERTICAL = Component.VERTICAL;
        /**
         * HORIZONTAL
         */
        int HORIZONTAL = Component.HORIZONTAL;
    }


    /**
     * onTouchEvent
     *
     * @param radius1
     */
    public void setRadius(int radius1) {
        this.radius = Math.max(radius1, MIN_RADIUS);
        onArrange();
    }

    /**
     * onTouchEvent
     *
     * @param peekDistance1
     */
    public void setPeekDistance(int peekDistance1) {
        this.peekDistance = Math.min(Math.max(peekDistance1, MIN_PEEK), radius);
        onArrange();
    }

    /**
     * onTouchEvent
     *
     * @param gravity1
     */
    public void setGravity(@Gravity int gravity1) {
        this.gravity = gravity1;
        onArrange();
    }

    /**
     * onTouchEvent
     *
     * @param isRo
     */
    public void setRotate(boolean isRo) {
        this.isRotate = isRo;
        onArrange();
    }

    /**
     * onArrange
     */
    public void onArrange() {
        this.center = deriveCenter(gravity, getOrientation(), radius, peekDistance, center);
        setChildOffsets();
    }

    /**
     * onRefreshed
     *
     * @param component
     */
    @Override
    public void onRefreshed(Component component) {
        onArrange();
    }

    /**
     * onContentScrolled
     *
     * @param component
     * @param scrollX
     * @param scrollY
     * @param oldScrollX
     * @param oldScrollY
     */
    @Override
    public void onContentScrolled(Component component, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
        int offsetX = Math.abs(scrollX - oldScrollX);
        int offsetY = Math.abs(scrollY - oldScrollY);
        if (offsetX > HALF * offsetY) {
            setChildOffsetsHorizontal();
        } else if (offsetY > HALF * offsetX) {
            setChildOffsetsVertical();
        }
    }

    /**
     * deriveCenter
     *
     * @param gravit
     * @param orientation
     * @param radius1
     * @param peekDistance1
     * @param out
     * @return point
     */
    private Point deriveCenter(@Gravity int gravit,
                               @Orientation int orientation,
                               int radius1,
                               int peekDistance1,
                               Point out) {
        final int gravitySign = gravit == Gravity.START ? COEFFICIENT : 1;
        final int distanceMultiplier = gravit == Gravity.START ? 0 : 1;
        int px;
        int py;
        switch (orientation) {
            case Orientation.HORIZONTAL:
                py = (distanceMultiplier * getHeight()) + gravitySign * (Math.abs(radius1 - peekDistance1));
                px = getWidth() / HALF;
                break;
            case Orientation.VERTICAL:
            default:
                py = getHeight() / HALF;
                px = (distanceMultiplier * getWidth()) + gravitySign * (Math.abs(radius1 - peekDistance1));
                break;
        }
        out.modify(px, py);
        return out;
    }

    /**
     * resolveOffsetX
     *
     * @param radius1
     * @param viewY
     * @param center1
     * @param peekDistance1
     * @return count
     */
    private double resolveOffsetX(double radius1, double viewY, Point center1, int peekDistance1) {
        final double opposite = Math.abs(center1.getPointY() - viewY);
        final double radiusSquared = radius1 * radius1;
        final double oppositeSquared = opposite * opposite;
        final double adjacentSideLength = Math.sqrt(radiusSquared - oppositeSquared);
        return adjacentSideLength - radius1 + peekDistance1;
    }

    /**
     * resolveOffsetY
     *
     * @param radius1
     * @param viewX
     * @param center1
     * @param peekDistance1
     * @return count
     */
    private double resolveOffsetY(double radius1, double viewX, Point center1, int peekDistance1) {
        final double adjacent = Math.abs(center1.getPointX() - viewX);
        final double radiusSquared = radius1 * radius1;
        final double adjacentSquared = adjacent * adjacent;
        final double oppositeSideLength = Math.sqrt(radiusSquared - adjacentSquared);
        return oppositeSideLength - radius1 + peekDistance1;
    }

    /**
     * resolveOffsetY
     */
    private void setChildOffsets() {
        if (getOrientation() == Component.VERTICAL) {
            setChildOffsetsVertical();
        } else if (getOrientation() == Component.HORIZONTAL) {
            setChildOffsetsHorizontal();
        }
    }

    /**
     * resolveOffsetY
     */
    public void setChildOffsetsVertical() {
        for (int ii = 0; ii < getChildCount(); ii++) {
            Component child = getComponentAt(ii);
            if (child == null) {
                continue;
            }
            LayoutConfig layoutParams = child.getLayoutConfig();
            final int offsetX = (int) resolveOffsetX(radius, DecimalUtils.add(child.getContentPositionY(),
                    child.getHeight() / PART),
                    center, peekDistance);
            final int xx = gravity == Gravity.START ? offsetX + layoutParams.getMarginLeft()
                    : getWidth() - offsetX - child.getWidth() - getMarginStart(layoutParams);
            child.setTranslationX(xx);
            setChildRotationVertical(gravity, child, radius, center);
        }
    }

    /**
     * setChildRotationVertical
     *
     * @param gravit
     * @param child
     * @param radius1
     * @param center1
     */
    private void setChildRotationVertical(@Gravity int gravit, Component child, int radius1, Point center1) {
        if (!isRotate) {
            child.setRotation(0);
            return;
        }
        boolean childPastCenter = (DecimalUtils.add(child.getContentPositionY(),
                child.getHeight() / PART)) > center1.getPointY();
        float directionMult;
        if (gravit == Gravity.END) {
            directionMult = childPastCenter ? COEFFICIENT : 1;
        } else {
            directionMult = childPastCenter ? 1 : COEFFICIENT;
        }
        final float opposite = Math.abs(DecimalUtils.sub(DecimalUtils.sub(child.getContentPositionY(),
                child.getHeight() / PART), center1.getPointY()));
        child.setRotation((float) (directionMult * Math.toDegrees(Math.asin(opposite / radius1))));
    }

    /**
     * setChildRotationVertical
     */
    private void setChildOffsetsHorizontal() {
        for (int ii = 0; ii < getChildCount(); ii++) {
            Component child = getComponentAt(ii);
            if (child == null) {
                continue;
            }
            LayoutConfig layoutParams = child.getLayoutConfig();
            final int offsetY = (int) resolveOffsetY(radius, DecimalUtils.add(child.getContentPositionX(),
                    child.getWidth() / PART),
                    center, peekDistance);
            final int yy = gravity == Gravity.START ? offsetY + getMarginStart(layoutParams)
                    : getHeight() - offsetY - child.getHeight() - getMarginStart(layoutParams);
            child.setTranslationY(yy);
            setChildRotationHorizontal(gravity, child, radius, center);
        }
    }

    /**
     * setChildRotationHorizontal
     *
     * @param gravit
     * @param child
     * @param radius1
     * @param center1
     */
    private void setChildRotationHorizontal(@Gravity int gravit, Component child, int radius1, Point center1) {
        if (!isRotate) {
            child.setRotation(0);
            return;
        }
        boolean childPastCenter = (DecimalUtils.add(child.getContentPositionX(),
                (float) child.getWidth() / HALF)) > center1.getPointX();
        float directionMult;
        if (gravit == Gravity.END) {
            directionMult = childPastCenter ? 1 : COEFFICIENT;
        } else {
            directionMult = childPastCenter ? COEFFICIENT : 1;
        }
        final float opposite = Math.abs(DecimalUtils.sub(DecimalUtils.add(child.getContentPositionX(),
                (float) child.getWidth() / HALF), center1.getPointX()));
        child.setRotation((float) (directionMult * Math.toDegrees(Math.asin(opposite / radius1))));
    }

    /**
     * setChildRotationHorizontal
     *
     * @param layoutParams
     * @return count
     */
    private int getMarginStart(LayoutConfig layoutParams) {
        return layoutParams.getMarginLeft();
    }
}
